/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.servlet;

import static org.apache.solr.common.params.CommonParams.PATH;

import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.Part;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.input.CloseShieldInputStream;
import org.apache.lucene.util.IOUtils;
import org.apache.solr.api.V2HttpCall;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.MultiMapSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.common.util.ContentStreamBase;
import org.apache.solr.common.util.FastInputStream;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.RequestHandlers;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrQueryRequestBase;
import org.apache.solr.util.RTimerTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SolrRequestParsers {

  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  // Should these constants be in a more public place?
  public static final String MULTIPART = "multipart";
  public static final String FORMDATA = "formdata";
  public static final String RAW = "raw";
  public static final String SIMPLE = "simple";
  public static final String STANDARD = "standard";

  private static final Charset CHARSET_US_ASCII = StandardCharsets.US_ASCII;

  public static final String INPUT_ENCODING_KEY = "ie";
  private static final byte[] INPUT_ENCODING_BYTES = INPUT_ENCODING_KEY.getBytes(CHARSET_US_ASCII);

  public static final String REQUEST_TIMER_SERVLET_ATTRIBUTE = "org.apache.solr.RequestTimer";

  private final HashMap<String, SolrRequestParser> parsers = new HashMap<>();
  private final boolean enableRemoteStreams;
  private final boolean enableStreamBody;
  private StandardRequestParser standard;

  /**
   * Default instance for e.g. admin requests. Limits to 2 MB uploads and does not allow remote
   * streams.
   */
  public static final SolrRequestParsers DEFAULT = new SolrRequestParsers();

  /**
   * Pass in an xml configuration. A null configuration will enable everything with maximum values.
   */
  public SolrRequestParsers(SolrConfig globalConfig) {
    final int multipartUploadLimitKB, formUploadLimitKB;
    if (globalConfig == null) {
      multipartUploadLimitKB = formUploadLimitKB = Integer.MAX_VALUE;
      enableRemoteStreams = false;
      enableStreamBody = false;

    } else {
      multipartUploadLimitKB = globalConfig.getMultipartUploadLimitKB();

      formUploadLimitKB = globalConfig.getFormUploadLimitKB();

      // security risks; disabled by default
      enableRemoteStreams = Boolean.getBoolean("solr.requests.streaming.remote.enabled");
      enableStreamBody = Boolean.getBoolean("solr.requests.streaming.body.enabled");

      // Let this filter take care of /select?xxx format

    }
    init(multipartUploadLimitKB, formUploadLimitKB);
  }

  private SolrRequestParsers() {
    enableRemoteStreams = false;
    enableStreamBody = false;

    init(Integer.MAX_VALUE, Integer.MAX_VALUE);
  }

  private void init(int multipartUploadLimitKB, int formUploadLimitKB) {
    MultipartRequestParser multi = new MultipartRequestParser(multipartUploadLimitKB);
    RawRequestParser raw = new RawRequestParser();
    FormDataRequestParser formdata = new FormDataRequestParser(formUploadLimitKB);
    standard = new StandardRequestParser(multi, raw, formdata);

    // I don't see a need to have this publicly configured just yet
    // adding it is trivial
    parsers.put(MULTIPART, multi);
    parsers.put(FORMDATA, formdata);
    parsers.put(RAW, raw);
    parsers.put(SIMPLE, new SimpleRequestParser());
    parsers.put(STANDARD, standard);
    parsers.put("", standard);
  }

  private static RTimerTree getRequestTimer(HttpServletRequest req) {
    final Object reqTimer = req.getAttribute(REQUEST_TIMER_SERVLET_ATTRIBUTE);
    if (reqTimer != null && reqTimer instanceof RTimerTree) {
      return ((RTimerTree) reqTimer);
    }

    return new RTimerTree();
  }

  public SolrQueryRequest parse(SolrCore core, String path, HttpServletRequest req)
      throws Exception {
    SolrRequestParser parser = standard;

    // TODO -- in the future, we could pick a different parser based on the request

    // Pick the parser from the request...
    ArrayList<ContentStream> streams = new ArrayList<>(1);
    SolrParams params = parser.parseParamsAndFillStreams(req, streams);

    SolrQueryRequest sreq =
        buildRequestFrom(core, params, streams, getRequestTimer(req), req, req.getUserPrincipal());

    // Handlers and login will want to know the path. If it contains a ':'
    // the handler could use it for RESTful URLs
    sreq.getContext().put(PATH, RequestHandlers.normalize(path));
    sreq.getContext().put("httpMethod", req.getMethod());
    return sreq;
  }

  /** For embedded Solr use; not related to HTTP. */
  public SolrQueryRequest buildRequestFrom(
      SolrCore core, SolrParams params, Collection<ContentStream> streams) throws Exception {
    return buildRequestFrom(core, params, streams, new RTimerTree(), null, null);
  }

  public SolrQueryRequest buildRequestFrom(
      SolrCore core, SolrParams params, Collection<ContentStream> streams, Principal principal)
      throws Exception {
    return buildRequestFrom(core, params, streams, new RTimerTree(), null, principal);
  }

  private SolrQueryRequest buildRequestFrom(
      SolrCore core,
      SolrParams params,
      Collection<ContentStream> streams, // might be added to but caller shouldn't depend on it
      RTimerTree requestTimer,
      final HttpServletRequest req,
      final Principal principal) // from req, if req was provided, otherwise from elsewhere
      throws Exception {
    // ensure streams is non-null and mutable so we can easily add to it
    if (streams == null) {
      streams = new ArrayList<>();
    } else if (!(streams instanceof ArrayList)) {
      streams = new ArrayList<>(streams);
    }

    // The content type will be applied to all streaming content
    String contentType = params.get(CommonParams.STREAM_CONTENTTYPE);

    // Handle anything with a remoteURL
    String[] strs = params.getParams(CommonParams.STREAM_URL);
    if (strs != null) {
      if (!enableRemoteStreams) {
        throw new SolrException(ErrorCode.BAD_REQUEST, "Remote Streaming is disabled.");
      }
      for (final String url : strs) {
        ContentStreamBase stream = new ContentStreamBase.URLStream(URI.create(url).toURL());
        if (contentType != null) {
          stream.setContentType(contentType);
        }
        streams.add(stream);
      }
    }

    // Handle streaming files
    strs = params.getParams(CommonParams.STREAM_FILE);
    if (strs != null) {
      if (!enableRemoteStreams) {
        throw new SolrException(
            ErrorCode.BAD_REQUEST,
            "Remote Streaming is disabled. See https://solr.apache.org/guide/solr/latest/configuration-guide/requestdispatcher.html for help");
      }
      for (final String file : strs) {
        ContentStreamBase stream = new ContentStreamBase.FileStream(Path.of(file));
        if (contentType != null) {
          stream.setContentType(contentType);
        }
        streams.add(stream);
      }
    }

    // Check for streams in the request parameters
    strs = params.getParams(CommonParams.STREAM_BODY);
    if (strs != null) {
      if (!enableStreamBody) {
        throw new SolrException(
            ErrorCode.BAD_REQUEST,
            "Stream Body is disabled. See https://solr.apache.org/guide/solr/latest/configuration-guide/requestdispatcher.html for help");
      }
      for (final String body : strs) {
        ContentStreamBase stream = new ContentStreamBase.StringStream(body);
        if (contentType != null) {
          stream.setContentType(contentType);
        }
        streams.add(stream);
      }
    }

    final HttpSolrCall httpSolrCall =
        req == null ? null : (HttpSolrCall) req.getAttribute(HttpSolrCall.class.getName());
    SolrQueryRequestBase q =
        new SolrQueryRequestBase(core, params, requestTimer) {
          @Override
          public Principal getUserPrincipal() {
            return principal;
          }

          @Override
          public CoreContainer getCoreContainer() {
            return httpSolrCall != null ? httpSolrCall.cores : super.getCoreContainer();
          }

          @Override
          public List<CommandOperation> getCommands(boolean validateInput) {
            if (httpSolrCall != null) {
              return httpSolrCall.getCommands(validateInput);
            }
            return super.getCommands(validateInput);
          }

          @Override
          public Map<String, String> getPathTemplateValues() {
            if (httpSolrCall != null && httpSolrCall instanceof V2HttpCall) {
              return ((V2HttpCall) httpSolrCall).getUrlParts();
            }
            return super.getPathTemplateValues();
          }

          @Override
          public HttpSolrCall getHttpSolrCall() {
            return httpSolrCall;
          }
        };
    if (!streams.isEmpty()) {
      q.setContentStreams(streams);
    }
    return q;
  }

  private static HttpSolrCall getHttpSolrCall(HttpServletRequest req) {
    return req == null ? null : (HttpSolrCall) req.getAttribute(HttpSolrCall.class.getName());
  }

  /** Given a url-encoded query string (UTF-8), map it into solr params */
  public static MultiMapSolrParams parseQueryString(String queryString) {
    Map<String, String[]> map = new HashMap<>();
    parseQueryString(queryString, map);
    return new MultiMapSolrParams(map);
  }

  /**
   * Given a url-encoded query string (UTF-8), map it into the given map
   *
   * @param queryString as given from URL
   * @param map place all parameters in this map
   */
  static void parseQueryString(final String queryString, final Map<String, String[]> map) {
    if (queryString != null && queryString.length() > 0) {
      try {
        final int len = queryString.length();
        // this input stream emulates to get the raw bytes from the URL as passed to servlet
        // container, it disallows any byte > 127 and enforces to %-escape them:
        final InputStream in =
            new InputStream() {
              int pos = 0;

              @Override
              public int read() {
                if (pos < len) {
                  final char ch = queryString.charAt(pos);
                  if (ch > 127) {
                    throw new SolrException(
                        ErrorCode.BAD_REQUEST,
                        "URLDecoder: The query string contains a not-%-escaped byte > 127 at position "
                            + pos);
                  }
                  pos++;
                  return ch;
                } else {
                  return -1;
                }
              }
            };
        parseFormDataContent(in, Long.MAX_VALUE, StandardCharsets.UTF_8, map, true);
      } catch (IOException ioe) {
        throw new SolrException(ErrorCode.BAD_REQUEST, ioe);
      }
    }
  }

  /**
   * Given a url-encoded form from POST content (as InputStream), map it into the given map. The
   * given InputStream should be buffered!
   *
   * @param postContent to be parsed
   * @param charset to be used to decode resulting bytes after %-decoding
   * @param map place all parameters in this map
   */
  @SuppressWarnings({"fallthrough", "resource"})
  static long parseFormDataContent(
      final InputStream postContent,
      final long maxLen,
      Charset charset,
      final Map<String, String[]> map,
      boolean supportCharsetParam)
      throws IOException {
    CharsetDecoder charsetDecoder = supportCharsetParam ? null : getCharsetDecoder(charset);
    final List<Object> buffer = supportCharsetParam ? new ArrayList<>() : null;
    long len = 0L, keyPos = 0L, valuePos = 0L;
    final ByteArrayOutputStream keyStream = new ByteArrayOutputStream(),
        valueStream = new ByteArrayOutputStream();
    ByteArrayOutputStream currentStream = keyStream;
    for (; ; ) {
      int b = postContent.read();
      switch (b) {
        case -1: // end of stream
        case '&': // separator
          if (keyStream.size() > 0) {
            final byte[] keyBytes = keyStream.toByteArray(), valueBytes = valueStream.toByteArray();
            if (Arrays.equals(keyBytes, INPUT_ENCODING_BYTES)) {
              // we found a charset declaration in the raw bytes
              if (charsetDecoder != null) {
                throw new SolrException(
                    ErrorCode.BAD_REQUEST,
                    supportCharsetParam
                        ? ("Query string invalid: duplicate '"
                            + INPUT_ENCODING_KEY
                            + "' (input encoding) key.")
                        : ("Key '"
                            + INPUT_ENCODING_KEY
                            + "' (input encoding) cannot "
                            + "be used in POSTed application/x-www-form-urlencoded form data. "
                            + "To set the input encoding of POSTed form data, use the "
                            + "'Content-Type' header and provide a charset!"));
              }
              // decode the charset from raw bytes
              charset =
                  Charset.forName(
                      decodeChars(valueBytes, keyPos, getCharsetDecoder(CHARSET_US_ASCII)));
              charsetDecoder = getCharsetDecoder(charset);
              // finally decode all buffered tokens
              decodeBuffer(buffer, map, charsetDecoder);
            } else if (charsetDecoder == null) {
              // we have no charset decoder until now, buffer the keys / values for later
              // processing:
              buffer.add(keyBytes);
              buffer.add(Long.valueOf(keyPos));
              buffer.add(valueBytes);
              buffer.add(Long.valueOf(valuePos));
            } else {
              // we already have a charsetDecoder, so we can directly decode without buffering:
              final String key = decodeChars(keyBytes, keyPos, charsetDecoder),
                  value = decodeChars(valueBytes, valuePos, charsetDecoder);
              MultiMapSolrParams.addParam(key.trim(), value, map);
            }
          } else if (valueStream.size() > 0) {
            throw new SolrException(
                ErrorCode.BAD_REQUEST, "application/x-www-form-urlencoded invalid: missing key");
          }
          keyStream.reset();
          valueStream.reset();
          keyPos = valuePos = len + 1;
          currentStream = keyStream;
          break;
        case '+': // space replacement
          currentStream.write(' ');
          break;
        case '%': // escape
          final int upper = digit16(b = postContent.read());
          len++;
          final int lower = digit16(b = postContent.read());
          len++;
          currentStream.write(((upper << 4) + lower));
          break;
        case '=': // kv separator
          if (currentStream == keyStream) {
            valuePos = len + 1;
            currentStream = valueStream;
            break;
          }
          // fall-through
        default:
          currentStream.write(b);
      }
      if (b == -1) {
        break;
      }
      len++;
      if (len > maxLen) {
        throw new SolrException(
            ErrorCode.BAD_REQUEST,
            "application/x-www-form-urlencoded content exceeds upload limit of "
                + (maxLen / 1024L)
                + " KB");
      }
    }
    // if we have not seen a charset declaration, decode the buffer now using the default one (UTF-8
    // or given via Content-Type):
    if (buffer != null && !buffer.isEmpty()) {
      assert charsetDecoder == null;
      decodeBuffer(buffer, map, getCharsetDecoder(charset));
    }
    return len;
  }

  private static CharsetDecoder getCharsetDecoder(Charset charset) {
    return charset
        .newDecoder()
        .onMalformedInput(CodingErrorAction.REPORT)
        .onUnmappableCharacter(CodingErrorAction.REPORT);
  }

  private static String decodeChars(byte[] bytes, long position, CharsetDecoder charsetDecoder) {
    try {
      return charsetDecoder.decode(ByteBuffer.wrap(bytes)).toString();
    } catch (CharacterCodingException cce) {
      throw new SolrException(
          ErrorCode.BAD_REQUEST,
          "URLDecoder: Invalid character encoding detected after position "
              + position
              + " of query string / form data (while parsing as "
              + charsetDecoder.charset().name()
              + ")");
    }
  }

  private static void decodeBuffer(
      final List<Object> input, final Map<String, String[]> map, CharsetDecoder charsetDecoder) {
    for (final Iterator<Object> it = input.iterator(); it.hasNext(); ) {
      final byte[] keyBytes = (byte[]) it.next();
      it.remove();
      final Long keyPos = (Long) it.next();
      it.remove();
      final byte[] valueBytes = (byte[]) it.next();
      it.remove();
      final Long valuePos = (Long) it.next();
      it.remove();
      MultiMapSolrParams.addParam(
          decodeChars(keyBytes, keyPos.longValue(), charsetDecoder).trim(),
          decodeChars(valueBytes, valuePos.longValue(), charsetDecoder),
          map);
    }
  }

  private static int digit16(int b) {
    if (b == -1) {
      throw new SolrException(
          ErrorCode.BAD_REQUEST, "URLDecoder: Incomplete trailing escape (%) pattern");
    }
    if (b >= '0' && b <= '9') {
      return b - '0';
    }
    if (b >= 'A' && b <= 'F') {
      return b - ('A' - 10);
    }
    if (b >= 'a' && b <= 'f') {
      return b - ('a' - 10);
    }
    throw new SolrException(
        ErrorCode.BAD_REQUEST,
        "URLDecoder: Invalid digit (" + ((char) b) + ") in escape (%) pattern");
  }

  public boolean isEnableRemoteStreams() {
    return enableRemoteStreams;
  }

  // -----------------------------------------------------------------
  // -----------------------------------------------------------------

  // I guess we don't really even need the interface, but i'll keep it here just for kicks
  interface SolrRequestParser {
    public SolrParams parseParamsAndFillStreams(
        final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception;
  }

  // -----------------------------------------------------------------
  // -----------------------------------------------------------------

  /** The simple parser just uses the params directly, does not support POST URL-encoded forms */
  static class SimpleRequestParser implements SolrRequestParser {
    @Override
    public SolrParams parseParamsAndFillStreams(
        final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception {
      return parseQueryString(req.getQueryString());
    }
  }

  /** Wrap an HttpServletRequest as a ContentStream */
  static class HttpRequestContentStream extends ContentStreamBase {
    private final InputStream inputStream;

    public HttpRequestContentStream(HttpServletRequest req, InputStream inputStream) {
      this.inputStream = inputStream;

      this.contentType = req.getContentType();
      // name = ???
      // sourceInfo = ???

      String v = req.getHeader("Content-Length");
      if (v != null) {
        size = Long.valueOf(v);
      }
    }

    @Override
    public InputStream getStream() throws IOException {
      // we explicitly protect this servlet stream from being closed
      // so that it does not trip our test assert in our close shield
      // in SolrDispatchFilter - we must allow closes from getStream
      // due to the other impls of ContentStream
      return new CloseShieldInputStream(inputStream);
    }
  }

  /** The raw parser just uses the params directly */
  static class RawRequestParser implements SolrRequestParser {

    // Methods that shouldn't have a body according to HTTP spec
    private static Set<String> NO_BODY_METHODS = Set.of("GET", "HEAD", "DELETE");

    @Override
    public SolrParams parseParamsAndFillStreams(
        final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception {
      if (req.getContentLengthLong() > 0
          || req.getHeader("Transfer-Encoding") != null
          || !NO_BODY_METHODS.contains(req.getMethod())) {
        // If Content-Length > 0 OR Transfer-Encoding exists OR
        // it's a method that can have a body (POST/PUT/PATCH etc)
        streams.add(new HttpRequestContentStream(req, req.getInputStream()));
      }

      return parseQueryString(req.getQueryString());
    }
  }

  /** Extract Multipart streams */
  static class MultipartRequestParser implements SolrRequestParser {
    private final MultipartConfigElement multipartConfigElement;

    public MultipartRequestParser(int uploadLimitKB) {
      multipartConfigElement =
          new MultipartConfigElement(
              null, // temp dir (null=default)
              -1, // maxFileSize  (-1=none)
              uploadLimitKB * 1024L, // maxRequestSize
              100 * 1024); // fileSizeThreshold after which will go to disk
    }

    @Override
    public SolrParams parseParamsAndFillStreams(
        final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception {
      if (!isMultipart(req)) {
        throw new SolrException(
            ErrorCode.BAD_REQUEST, "Not multipart content! " + req.getContentType());
      }
      // Magic way to tell Jetty dynamically we want multi-part processing.
      // This is taken from:
      // https://github.com/eclipse/jetty.project/blob/jetty-10.0.12/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java#L144
      req.setAttribute("org.eclipse.jetty.multipartConfig", multipartConfigElement);

      MultiMapSolrParams params = parseQueryString(req.getQueryString());

      // IMPORTANT: the Parts will all have the delete() method called by cleanupMultipartFiles()

      for (Part part : req.getParts()) {
        if (part.getSubmittedFileName() == null) { // thus a form field and not file upload
          // If it's a form field, put it in our parameter map
          String partAsString = StrUtils.stringFromReader(new PartContentStream(part).getReader());
          MultiMapSolrParams.addParam(part.getName().trim(), partAsString, params.getMap());
        } else { // file upload
          streams.add(new PartContentStream(part));
        }
      }
      return params;
    }

    /** Wrap a MultiPart-{@link Part} as a {@link ContentStream} */
    static class PartContentStream extends ContentStreamBase {
      private final Part part;

      public PartContentStream(Part part) {
        this.part = part;
        contentType = part.getContentType();
        name = part.getName();
        sourceInfo = part.getSubmittedFileName();
        size = part.getSize();
      }

      @Override
      public InputStream getStream() throws IOException {
        return part.getInputStream();
      }
    }
  }

  public static boolean isMultipart(HttpServletRequest req) {
    String ct = req.getContentType();
    return ct != null && ct.startsWith("multipart/form-data");
  }

  /** Clean up any files created by MultiPartInputStream. */
  static void cleanupMultipartFiles(HttpServletRequest request) {
    if (!SolrRequestParsers.isMultipart(request)) {
      return;
    }

    log.debug("Deleting multipart files");

    Collection<Part> parts;
    try {
      parts = request.getParts();
    } catch (Exception e) {
      assert false : e.toString();
      log.error("Couldn't get multipart parts in order to delete them", e);
      return;
    }

    for (Part part : parts) {
      try {
        part.delete();
      } catch (IOException e) {
        log.warn("Errors deleting multipart tmp files", e);
      }
    }
  }

  /** Extract application/x-www-form-urlencoded form data for POST requests */
  static class FormDataRequestParser implements SolrRequestParser {
    private static final long WS_MASK =
        (1L << ' ')
            | (1L << '\t')
            | (1L << '\r')
            | (1L << '\n')
            | (1L << '#')
            | (1L << '/')
            | (0x01); // set 1 bit so 0xA0 will be flagged as possible whitespace

    private final int uploadLimitKB;

    public FormDataRequestParser(int limit) {
      uploadLimitKB = limit;
    }

    public SolrParams parseParamsAndFillStreams(
        HttpServletRequest req, ArrayList<ContentStream> streams, InputStream in) throws Exception {
      final Map<String, String[]> map = new HashMap<>();

      // also add possible URL parameters and include into the map (parsed using UTF-8):
      final String qs = req.getQueryString();
      if (qs != null) {
        parseQueryString(qs, map);
      }

      // may be -1, so we check again later. But if it's already greater we can stop processing!
      final long totalLength = req.getContentLength();
      final long maxLength = ((long) uploadLimitKB) * 1024L;
      if (totalLength > maxLength) {
        throw new SolrException(
            ErrorCode.BAD_REQUEST,
            "application/x-www-form-urlencoded content length ("
                + totalLength
                + " bytes) exceeds upload limit of "
                + uploadLimitKB
                + " KB");
      }

      // get query String from request body, using the charset given in content-type:
      final String cs = ContentStreamBase.getCharsetFromContentType(req.getContentType());
      final Charset charset = (cs == null) ? StandardCharsets.UTF_8 : Charset.forName(cs);

      try {
        // Protect container owned streams from being closed by us, see SOLR-8933
        in =
            FastInputStream.wrap(
                in == null
                    ? new CloseShieldInputStream(req.getInputStream())
                    : new CloseShieldInputStream(in));

        final long bytesRead = parseFormDataContent(in, maxLength, charset, map, false);
        if (bytesRead == 0L && totalLength > 0L) {
          throw getParameterIncompatibilityException();
        }
      } catch (IOException ioe) {
        throw new SolrException(ErrorCode.BAD_REQUEST, ioe);
      } catch (IllegalStateException ise) {
        throw (SolrException) getParameterIncompatibilityException().initCause(ise);
      } finally {
        IOUtils.closeWhileHandlingException(in);
      }

      return new MultiMapSolrParams(map);
    }

    @Override
    public SolrParams parseParamsAndFillStreams(
        HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception {
      if (!isFormData(req)) {
        throw new SolrException(
            ErrorCode.BAD_REQUEST,
            "Not application/x-www-form-urlencoded content: " + req.getContentType());
      }

      return parseParamsAndFillStreams(req, streams, null);
    }

    public static SolrException getParameterIncompatibilityException() {
      return new SolrException(
          ErrorCode.SERVER_ERROR,
          "Solr requires that request parameters sent using application/x-www-form-urlencoded "
              + "content-type can be read through the request input stream. Unfortunately, the "
              + "stream was empty / not available. This may be caused by another servlet filter calling "
              + "ServletRequest.getParameter*() before SolrDispatchFilter, please remove it.");
    }

    public boolean isFormData(HttpServletRequest req) {
      String contentType = req.getContentType();
      if (contentType != null) {
        int idx = contentType.indexOf(';');
        if (idx > 0) { // remove the charset definition "; charset=utf-8"
          contentType = contentType.substring(0, idx);
        }
        contentType = contentType.trim();
        if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
          return true;
        }
      }
      return false;
    }
  }

  /** The default Logic */
  static class StandardRequestParser implements SolrRequestParser {
    MultipartRequestParser multipart;
    RawRequestParser raw;
    FormDataRequestParser formdata;

    StandardRequestParser(
        MultipartRequestParser multi, RawRequestParser raw, FormDataRequestParser formdata) {
      this.multipart = multi;
      this.raw = raw;
      this.formdata = formdata;
    }

    @Override
    public SolrParams parseParamsAndFillStreams(
        final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception {
      String contentType = req.getContentType();
      String method = req.getMethod(); // No need to uppercase... HTTP verbs are case sensitive
      String uri = req.getRequestURI();
      boolean isV2 = getHttpSolrCall(req) instanceof V2HttpCall;
      boolean isPost = "POST".equals(method);

      // SOLR-6787 changed the behavior of a POST without content type.  Previously it would throw
      // an exception, but now it will use the raw request parser.
      /*
      if (contentType == null && isPost) {
        throw new SolrException(ErrorCode.UNSUPPORTED_MEDIA_TYPE,
            "Must specify a Content-Type header with POST requests");
      }
      */

      // According to previous StandardRequestParser logic (this is a re-written version),
      // POST was handled normally, but other methods (PUT/DELETE)
      // were handled by the RestManager classes if the URI contained /schema or /config
      if (!isPost) {
        if (isV2) {
          return raw.parseParamsAndFillStreams(req, streams);
        }
        if (contentType == null) {
          return parseQueryString(req.getQueryString());
        }

        // This happens when Jetty redirected a request that initially had no content body
        if (contentType.equals("application/octet-stream") && req.getContentLength() == 0) {
          return parseQueryString(req.getQueryString());
        }

        // OK, we have a BODY at this point

        boolean schemaRestPath = false;
        int idx = uri.indexOf("/schema");
        if ((idx >= 0 && uri.endsWith("/schema")) || uri.contains("/schema/")) {
          schemaRestPath = true;
        }

        if (schemaRestPath) {
          return raw.parseParamsAndFillStreams(req, streams);
        }

        if ("PUT".equals(method) || "DELETE".equals(method)) {
          throw new SolrException(
              ErrorCode.BAD_REQUEST, "Unsupported method: " + method + " for request " + req);
        }
      }

      if (formdata.isFormData(req)) {
        String userAgent = req.getHeader("User-Agent");
        boolean isCurl = userAgent != null && userAgent.startsWith("curl/");

        FastInputStream input = FastInputStream.wrap(req.getInputStream());

        if (isCurl) {
          SolrParams params = autodetect(req, streams, input);
          if (params != null) return params;
        }

        return formdata.parseParamsAndFillStreams(req, streams, input);
      }

      if (isMultipart(req)) {
        return multipart.parseParamsAndFillStreams(req, streams);
      }

      // some other content-type (json, XML, csv, etc)
      return raw.parseParamsAndFillStreams(req, streams);
    }
  }

  private static final long WS_MASK =
      (1L << ' ')
          | (1L << '\t')
          | (1L << '\r')
          | (1L << '\n')
          | (1L << '#')
          | (1L << '/')
          | (0x01); // set 1 bit so 0xA0 will be flagged as possible whitespace

  /** Returns the parameter map if a different content type was auto-detected */
  private static SolrParams autodetect(
      HttpServletRequest req, ArrayList<ContentStream> streams, FastInputStream in)
      throws IOException {
    String detectedContentType = null;
    boolean shouldClose = true;

    try {
      in.peek(); // should cause some bytes to be read
      byte[] arr = in.getBuffer();
      int pos = in.getPositionInBuffer();
      int end = in.getEndInBuffer();

      // we do "end-1" because we check "arr[i+1]" sometimes in the loop body
      for (int i = pos; i < end - 1; i++) {
        int ch = arr[i];
        boolean isWhitespace = ((WS_MASK >> ch) & 0x01) != 0 && (ch <= ' ' || ch == 0xa0);
        if (!isWhitespace) {
          // first non-whitespace chars
          if (ch == '#' // single line comment
              || (ch == '/'
                  && (arr[i + 1] == '/' || arr[i + 1] == '*')) // single line or multi-line comment
              || (ch == '{' || ch == '[') // start of JSON object
          ) {
            detectedContentType = "application/json";
          }
          if (ch == '<') {
            detectedContentType = "text/xml";
          }
          break;
        }
      }

      if (detectedContentType == null) {
        shouldClose = false;
        return null;
      }

      Long size = null;
      String v = req.getHeader("Content-Length");
      if (v != null) {
        size = Long.valueOf(v);
      }
      streams.add(new InputStreamContentStream(in, detectedContentType, size));

      final Map<String, String[]> map = new HashMap<>();
      // also add possible URL parameters and include into the map (parsed using UTF-8):
      final String qs = req.getQueryString();
      if (qs != null) {
        parseQueryString(qs, map);
      }

      return new MultiMapSolrParams(map);

    } catch (IOException ioe) {
      throw new SolrException(ErrorCode.BAD_REQUEST, ioe);
    } catch (IllegalStateException ise) {
      throw (SolrException)
          FormDataRequestParser.getParameterIncompatibilityException().initCause(ise);
    } finally {
      if (shouldClose) {
        IOUtils.closeWhileHandlingException(in);
      }
    }
  }

  /** Wrap InputStream as a ContentStream */
  static class InputStreamContentStream extends ContentStreamBase {
    private final InputStream is;

    public InputStreamContentStream(InputStream is, String detectedContentType, Long size) {
      this.is = is;
      this.contentType = detectedContentType;
      this.size = size;
    }

    @Override
    public InputStream getStream() throws IOException {
      return is;
    }
  }
}
